﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Text;

namespace SMARTMobile.WinPhone
{
    public class DataSourceRequest
    {
        public event Action<DataSourceRequest> OnPrepareRequest;
        public event Func<DataSourceRequest, Uri, Uri> OnGetUri;
        public event Action<Stream> OnWriteResponse;
        public event Action<DataSourceRequest> OnRequestComplete;

        public readonly Guid Key = Guid.NewGuid();
        public IDictionary<string, string> Parameters { get; private set; }

        public HttpWebRequest WebRequest { get; private set; }
        public HttpWebResponse WebResponse { get; internal set; }
        public readonly bool ObserveOnDispatcher;
        public readonly string ElementName;
        public readonly DataSourceType SourceType;

        private readonly Uri _baseUri;
        private IDisposable _subscription;
        private static readonly IDictionary<Guid, DataSourceRequest> CurrentSubscriptions;
        private static readonly object SyncRoot = new object();

        static DataSourceRequest()
        {
            CurrentSubscriptions = new Dictionary<Guid, DataSourceRequest>();
        }

        public Guid SubscribeImage(IObserver<Stream> observer)
        {
            Debug.Assert(null != observer, "Invalid IObserver");
            var observable = DataClientService.GetImage(GetUrl());
            _subscription = observable.Finally(() => CleanupSubscription(Key)).Subscribe(observer);
            return AddNewSubscription(this);
        }

        public Guid SubscribeBytes(IObserver<byte[]> observer)
        {
            Debug.Assert(null != observer, "Invalid IObserver");
            WebRequest = DataClientService.GetWebRequest(GetUrl(), false);
            if (null != OnPrepareRequest) OnPrepareRequest(this);
            var observableBytes = DataClientService.GetContentBytes(this);
            _subscription = observableBytes.Finally(() => CleanupSubscription(Key)).Subscribe(observer);
            return AddNewSubscription(this);
        }

        public Guid Subscribe<T>(IObserver<T> observer) where T : class, new()
        {
            Debug.Assert(null != observer, "Invalid IObserver");
            WebRequest = DataClientService.GetWebRequest(GetUrl());
            if (null != OnPrepareRequest) OnPrepareRequest(this);

#if DEBUG
            Debug.WriteLine("HttpWebRequest for {0}", WebRequest.RequestUri);
            Debug.WriteLine("HEADERS\n{0}\nHEADERS END", WebRequest.Headers);
#endif
            var observable = DataClientService.GetItems<T>(this);
            _subscription = observable.Finally(() => CleanupSubscription(Key)).Subscribe(observer);
            return AddNewSubscription(this);
        }

        private DataSourceRequest(Uri uri, string elementName, bool observeOnDispatcher, IDictionary<string, string> parameters)
        {
            _baseUri = uri;
            ElementName = elementName;
            ObserveOnDispatcher = observeOnDispatcher;
            Parameters = parameters ?? new Dictionary<string, string>();
        }

        private Uri GetUrl()
        {
            Uri uri = new Uri(_baseUri.ToString());
            if (null == Parameters || 0 == Parameters.Count)
            {
                if (null != OnGetUri) OnGetUri(this, uri);
                return uri;
            }
            UriBuilder builder = new UriBuilder(_baseUri) { Query = Parameters.GetQueryString() };
            uri = builder.Uri;
            if (null != OnGetUri)
            {
                uri = OnGetUri(this, uri);
            }
            return uri;
        }

        public static DataSourceRequest Create(Uri uri)
        {
            return new DataSourceRequest(uri, null, true, null);
        }

        public static DataSourceRequest Create(DataSource source, bool observeOnDispatcher)
        {
            Uri uri = source.Endpoint.GetUri();
            if (null != source.Paramaters && source.Paramaters.Count > 0)
            {
                IDictionary<string, string> parameters = source.Paramaters.ToDictionary(p => p.Name, p => p.Value);
                return new DataSourceRequest(uri, source.Element, observeOnDispatcher, parameters);
            }
            return new DataSourceRequest(uri, source.Element, observeOnDispatcher, null);
        }

        private void Dispose()
        {
            if (null != _subscription) _subscription.Dispose();
        }

        public static Guid AddNewSubscription(DataSourceRequest request)
        {
            lock (SyncRoot)
            {
                CurrentSubscriptions.Add(request.Key, request);
                return request.Key;
            }
        }

        public void CleanupSubscription(Guid key)
        {
            if (null != OnRequestComplete) OnRequestComplete(this);
            lock (SyncRoot)
            {
                if (!CurrentSubscriptions.ContainsKey(key))
                {
                    Debug.WriteLine("_currentSubscriptions does not contain key");
                    return;
                }
                CurrentSubscriptions[key].Dispose();
                CurrentSubscriptions.Remove(key);
            }
        }

        public static void DisposeSubscriptions()
        {
            lock (SyncRoot)
            {
                List<DataSourceRequest> requests = (from r in CurrentSubscriptions.Values select r).ToList();
                foreach (DataSourceRequest request in requests)
                {
                    request.Dispose();
                }
                CurrentSubscriptions.Clear();
            }
        }

        internal void FireOnWriteResponse(Stream stream)
        {
            if (null != OnWriteResponse) OnWriteResponse(stream);
        }
    }

    static class ParameterExtensions
    {
        public static string GetQueryString(this IDictionary<string, string> parameters)
        {
            StringBuilder sb = new StringBuilder();
            int count = parameters.Count;
            foreach (var pair in parameters)
            {
                string format = --count == 0 ? "{0}={1}" : "{0}={1}&";
                sb.AppendFormat(format, pair.Key, Uri.EscapeDataString(pair.Value));
            }
            return sb.ToString();
        }
    }
}
